<?php
namespace core\model;

use core\library\DB;
use core\utils\Bean;

/**
 * Class Model
 * @package core\model
 */
class Model implements Bean{
    /**
     * @var DB
     */
    protected $db;
    protected $cache; // 保留
    protected $table;
    protected $lastSQL = '';
    protected $lastParams = [];
    protected $pk = 'id'; // 主键名称

    /**
     * 通过__call进行调用的字段，如果没有设置，那就不做校验
     * @var string[]
     */
    protected $fieldList = [];

    const EQ = 'eq';       // 等于
    const NEQ = 'neq';     // 不等于
    const MORE = 'more';   // 大于
    const LESS = 'less';   // 小于
    const MORE_OR_EQ = 'moreoreq'; // 大于等于
    const LESS_OR_EQ = 'lessoreq';
    const IN = 'in';

    const AND = 'and';
    const OR = 'or';

    private $allowVt = [
        Model::EQ           => '=',
        Model::NEQ          => '!=',
        Model::MORE         => '>',
        Model::LESS         => '<',
        Model::MORE_OR_EQ   => '>=',
        Model::LESS_OR_EQ   => '<=',
        Model::IN           => 'in',
    ]; // 允许的$condition
    private $allowConj = [Model::AND, Model::IN]; // 允许使用的连词

    public function __construct() {
        $this->db = DB::getInstance(config('db'));
        $this->setTableName();
    }

    /**
     * 设置表名，这个表名依赖于配置项 db.config, 以及Model类的子类
     * 通过db.config获取表的前缀,后缀以及命名方式， 通过Model的子类的名称，获取表的名称(除前后缀部分), 然后再根据命名方式进行转换
     */
    private function setTableName() {
        $sonFullClass = get_called_class();
        $son = str_replace('Model', '', substr($sonFullClass, strrpos($sonFullClass, '\\') + 1));
        $config = config('db.table');

        $this->table = $config['prefix'].$this->convert($son, $config['name_case']).$config['suffix'];
    }

    /**
     *
     * @param $name
     * @param $arguments
     * @return array
     */
    public function __call($name, $arguments) {
        if (substr($name, 0, strlen('findBy')) === 'findBy') { // 查询单条数据
            // 分析方法名
            $fieldsArray = $this->analysisWolds($name);
            // 拼接sql语句
            $this->lastSQL = $this->buildSql($fieldsArray, $arguments).' limit 1';
            // 拼接
            $this->lastParams = $this->buildParam($fieldsArray, $arguments);
            // 执行
            $this->db->prepareAndExec($this->lastSQL, $this->lastParams);
            // 返回结果
            $res = $this->db->getResultSet();
            if (count($res) > 0)
                return $res[0];
            return [];
        } elseif (substr($name, 0, strlen('selectBy')) === 'selectBy') {
            $fieldsArray = $this->analysisWolds($name);
            $this->lastSQL = $this->buildSql($fieldsArray, $arguments);
            $this->lastParams = $this->buildParam($fieldsArray, $arguments);
            $this->db->prepareAndExec($this->lastSQL, $this->lastParams);
            return $this->db->getResultSet();
        } else {
            throw new \Error('调用的方法不存在');
        }
    }

    /**
     * 构建一条SQL语句
     * @param array $fieldArray 查询条件的字段数组
     * @param array $arguments  查询的参数数组
     * @return string           拼接完成的SQL语句
     * @throws \Error           参数个数与条件个数不一致
     */
    private function buildSql(array $fieldArray, array $arguments) {
        $fieldCount = count($fieldArray);
        $argsCount = count($arguments);
        // 检验参数个数
        if ($fieldCount != $argsCount && $fieldCount != $argsCount - 1)
            throw new \Error("参数个数不匹配");

        // 要查询的字段值
        if ($fieldCount == $argsCount - 1) {
            $fieldListStr = rtrim(implode($arguments[$argsCount - 1], ','));
        } else {
            $fieldListStr = '*';
        }

        $sql = "select {$fieldListStr} from {$this->table} ";
        // 拼接where
        if (count($fieldArray) > 0) {
            $sql .= ' where ';
        }
        // 拼接查询条件
        foreach ($fieldArray as $index=>$value) {
            switch ($value['vt']) {
                case Model::EQ :
                case Model::NEQ :
                case Model::MORE:
                case Model::LESS:
                case Model::MORE_OR_EQ:
                case Model::LESS_OR_EQ:
                    $sql .= $value['field']. $this->allowVt[$value['vt']] .'?';
                    break;
                case Model::IN:
                    $sql .= $value['field'].' in ('.rtrim(str_repeat('?,', count($arguments[$index])), ',').')';
                    break;
            }
            if (isset($value['conj']))
                $sql .= ' '. $value['conj'].' ';
        }
        return $sql;
    }

    /**
     * 构建上一个SQL语句所需的参数
     * @param array $fieldsArray 查询条件的字段数组
     * @param array $arguments  传入的参数
     * @return array            构建完成之后的参数
     */
    private function buildParam(array $fieldsArray, $arguments) {
        if (count($fieldsArray) < count($arguments)) {
            unset($arguments[count($arguments) - 1]);
        }
        $res = [];
        foreach ($arguments as $argument) {
            if (is_string($argument) || is_numeric($argument))
                $res[] = $argument;
            elseif (is_array($argument))
                $res += $argument;
        }
        return $res;
    }

    /**
     * 按照大写字母把$name分割成数组
     * @param string $name    要分割的字符串
     * @return array          分割之后的字符串
     */
    private function analysisWolds($name) {
        $splitChar = ",";
        $formatStr = preg_replace("/([A-Z])/", ",\\1", $name);
        $wordList = explode($splitChar, $formatStr);
        $fieldArray = [];
        for ($i = 2; $i < count($wordList); $i += 3) {
            $index = (int)(($i - 2) / 3);

            // field
            $field = strtolower(lcfirst($wordList[$i + 0]));
            if (count($this->fieldList) && !in_array($field, $this->fieldList))
                throw new \Error("不被允许的字段");
            $fieldArray[$index]['field'] = $field;


            // vt
            $vt = strtolower($wordList[$i + 1]);
            if (array_key_exists($vt, $this->allowVt))
                $fieldArray[$index]['vt'] = $vt;
            else
                throw new \Error('不被允许的vt');

            // conj
            if (!isset($wordList[$i + 2]))
                break;
            $conj = strtolower($wordList[$i + 2]);
            if (in_array($conj, $this->allowConj))
                $fieldArray[$index]['conj'] = $conj;
            else
                throw new \Error('不被允许的conj');
        }
        return $fieldArray;
    }

    /**
     * 根据配置的命名规则，把 $son 转换成符合规范的表名
     * @param string $son       需要转换的名称
     * @param string $nameCase  命名规范
     * @return string           转换之后的字符串
     */
    private function convert(string $son, $nameCase) : string {
        switch ($nameCase) {
            case 'BigCamelCase':
                return $son;
            case 'SmallCamelCase':
                return lcfirst($son);
            default : // 'UnderScoreCase'
                return strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $son));
        }
    }

    /**
     * 通过主键查询一条信息，见<code>findByField</code>
     * @param string|int $pk                 主键
     * @param array      $returnFieldList    要查询的字段
     * @return array
     */
    public function findByPk($pk, array $returnFieldList = []) {
        return $this->findByField($this->pk, $pk, $returnFieldList);
    }

    /**
     * 通过某字段查询一条信息，注意，在这个方法里面，是直接通过implode来进行字段合并的，
     * 因此存在SQL注入的风险，不要使用前端传入的field
     * @param string     $fieldName          查询的条件字段
     * @param string|int $value              查询的条件字段的值
     * @param array      $returnFieldList    要查询的字段
     * @return array
     */
    public function findByField(string $fieldName, $value, array $returnFieldList) {
        // 切分返回的字段列表
        $fieldStr = '';
        if (count($returnFieldList) == 0) {
            $fieldStr = '*';
        } else {
            $fieldStr = rtrim(implode(",", $returnFieldList), ',');
            // 删除field里面可能的注释字符
            $fieldStr = str_replace(['#', '-- '], ['', ''], $fieldStr);
        }
        // 拼接查询的sql语句
        $this->lastSQL = "select {$fieldStr} from {$this->table} where {$fieldName}=? limit 1";
        // 查询
        $this->db->prepareAndExec($this->lastSQL, [$value]);
        // 返回结果
        $res = $this->db->getResultSet();
        if (count($res) > 0)
            return $res[0];
        return [];
    }

    /**
     * 获取上一次__call执行的sql语句
     * @return string
     */
    public function getLastSQL(): string
    {
        return $this->lastSQL;
    }

    /**
     * 获取上一次执行的参数
     * @return array
     */
    public function getLastParams(): array
    {
        return $this->lastParams;
    }
}